<?php

    /**
     * Gd2
     *
     * @package      Core
     * @subpackage   Image_Adapter
     * 
     * @license      Open Software License (OSL 3.0) see LICENCE-OSL file or http://opensource.org/licenses/osl-3.0.php
     */
    class Core_Image_Adapter_Gd2 extends Core_Image_Adapter_Abstract
    {

    ////////////////////////////////////////////////////////////////////////////
    //                                Variables                               //
    ////////////////////////////////////////////////////////////////////////////

        protected $_requiredExtensions = array("gd");

        private static $_callbacks = array(
            IMAGETYPE_GIF  => array('output' => 'imagegif',  'create' => 'imagecreatefromgif',  'quality' => false),
            IMAGETYPE_JPEG => array('output' => 'imagejpeg', 'create' => 'imagecreatefromjpeg', 'quality' => true),
            IMAGETYPE_PNG  => array('output' => 'imagepng',  'create' => 'imagecreatefrompng',  'quality' => true),
            IMAGETYPE_XBM  => array('output' => 'imagexbm',  'create' => 'imagecreatefromxbm',  'quality' => false),
            IMAGETYPE_WBMP => array('output' => 'imagewbmp', 'create' => 'imagecreatefromxbm',  'quality' => false),
        );

        protected $_quality = 100;

    ////////////////////////////////////////////////////////////////////////////
    //                                 Methods                                //
    ////////////////////////////////////////////////////////////////////////////

        /**
         * getQuality
         *
         * @return      integer
         */
        public function getQuality()
        {
            if ($this->_getCallback('output') === 'imagepng') {
                // Quality is different for PNG image
                return ((100 - $this->_quality) * 9 / 100);
            } else {
                // Normal quality
                return $this->_quality;
            }
        }

        /**
         * setQuality
         *
         * @param       integer         $quality
         * @return      Core_Image
         */
        public function setQuality($quality)
        {
            $this->_quality = intval($quality);
            return $this;
        }

        /**
         * Destructor
         */
        public function __destruct()
        {
            @imagedestroy($this->_imageHandler);
        }

        public function open($filename)
        {
            $this->_fileName = $filename;
            $this->getMimeType();
            $this->_getFileAttributes();
            $this->_imageHandler = call_user_func($this->_getCallback('create'), $this->_fileName);
        }

        /**
         * save
         *
         * @param       string      $destination
         * @param       string      $newName
         * @throws      Core_Exception
         * return       void
         */
        public function save($destination = null, $newName = null)
        {
            $fileName = (!isset($destination)) ? $this->_fileName : $destination;

            if (isset($destination) && isset($newName)) {
                $fileName = $destination . '/' . $fileName;
            } else if (isset($destination) && !isset($newName)) {
                $info        = pathinfo($destination);
                $fileName    = $destination;
                $destination = $info['dirname'];
            } else if (!isset($destination) && isset($newName)) {
                $fileName = $this->_fileSrcPath . '/' . $newName;
            } else {
                $fileName = $this->_fileSrcPath . $this->_fileSrcName;
            }

            $destinationDir = (isset($destination)) ? $destination : $this->_fileSrcPath;

            if (!@is_writable($destinationDir)) {
                try {
                    Core_IO_Directory::createDirectory($destination);
                } catch (Exception $e) {
                    throw new Core_Exception("Unable to write file into directory '". $destinationDir ."'. Access forbidden");
                }
            }

            if ($this->_getCallback('quality') !== false) {
                call_user_func($this->_getCallback('output'), $this->_imageHandler, $fileName, $this->getQuality());
            } else {
                call_user_func($this->_getCallback('output'), $this->_imageHandler, $fileName);
            }
        }

        /**
         * display
         *
         * @return      void
         */
        public function display()
        {
            header("Content-type: ". $this->getMimeType());

            if ($this->_getCallback('quality')) {
                call_user_func($this->_getCallback('output'), $this->_imageHandler, $fileName, $this->_quality);
            } else {
                call_user_func($this->_getCallback('output'), $this->_imageHandler, $fileName);
            }
        }

        /**
         * Change the image size
         *
         * @param       integer     $frameWidth
         * @param       integer     $frameHeight
         * @throws      Core_Exception
         * @return      void
         */
        public function resize($frameWidth = null, $frameHeight = null)
        {
            if (empty($frameWidth) && empty($frameHeight)) {
                throw new Core_Exception('Invalid image dimensions');
            }

            // calculate lacking dimension
            if (!$this->_keepFrame) {
                if (null === $frameWidth) {
                    $frameWidth = round($frameHeight * ($this->_imageSrcWidth / $this->_imageSrcHeight));
                } else if (null === $frameHeight) {
                    $frameHeight = round($frameWidth * ($this->_imageSrcHeight / $this->_imageSrcWidth));
                }
            } else {
                if (null === $frameWidth) {
                    $frameWidth = $frameHeight;
                } else if (null === $frameHeight) {
                    $frameHeight = $frameWidth;
                }
            }

            // define coordinates of image inside new frame
            $srcX = 0;
            $srcY = 0;
            $dstX = 0;
            $dstY = 0;
            $dstWidth  = $frameWidth;
            $dstHeight = $frameHeight;

            if ($this->_keepAspectRatio) {
                // do not make picture bigger, than it is, if required
                if ($this->_constrainOnly) {
                    if (($frameWidth >= $this->_imageSrcWidth) && ($frameHeight >= $this->_imageSrcHeight)) {
                        $dstWidth  = $this->_imageSrcWidth;
                        $dstHeight = $this->_imageSrcHeight;
                    }
                }

                // keep aspect ratio
                if ($this->_imageSrcWidth / $this->_imageSrcHeight >= $frameWidth / $frameHeight) {
                    $dstHeight = round(($dstWidth / $this->_imageSrcWidth) * $this->_imageSrcHeight);
                } else {
                    $dstWidth = round(($dstHeight / $this->_imageSrcHeight) * $this->_imageSrcWidth);
                }
            }

            // define position in center (TODO: add positions option)
            $dstY = round(($frameHeight - $dstHeight) / 2);
            $dstX = round(($frameWidth - $dstWidth) / 2);

            // get rid of frame (fallback to zero position coordinates)
            if (!$this->_keepFrame) {
                $frameWidth  = $dstWidth;
                $frameHeight = $dstHeight;
                $dstY = 0;
                $dstX = 0;
            }

            // create new image
            $isAlpha     = false;
            $isTrueColor = false;
            $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha, $isTrueColor);

            if ($isTrueColor) {
                $newImage = imagecreatetruecolor($frameWidth, $frameHeight);
            } else {
                $newImage = imagecreate($frameWidth, $frameHeight);
            }

            // fill new image with required color
            $this->_fillBackgroundColor($newImage);

            // resample source image and copy it into new frame
            imagecopyresampled($newImage, $this->_imageHandler, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $this->_imageSrcWidth, $this->_imageSrcHeight);
            $this->_imageHandler = $newImage;
            $this->refreshImageDimensions();
        }

        /**
         * rotate
         *
         * @param       integer     $angle
         * @return      void
         */
        public function rotate($angle)
        {
            $this->_imageHandler = imagerotate($this->_imageHandler, $angle, $this->imageBackgroundColor);
            $this->refreshImageDimensions();
        }

        /**
         * Adds watermark to our image
         *
         * @param       string      $watermarkImage : absolute path to watermark image
         * @param       integer     $positionX : watermark X position
         * @param       integer     $positionY : watermark Y position
         * @param       integer     $watermarkImageOpacity : watermark image opacity
         * @param       boolean     $repeat : enable or disable watermark brick
         * @return      void
         */
        public function watermark($watermarkImage, $positionX = 0, $positionY = 0, $watermarkImageOpacity = 30, $repeat = false)
        {
            list($watermarkSrcWidth, $watermarkSrcHeight, $watermarkFileType, ) = getimagesize($watermarkImage);
            $this->_getFileAttributes();
            $watermark = call_user_func($this->_getCallback('create', $watermarkFileType, 'Unsupported watermark image format'), $watermarkImage);

            if ($this->getWatermarkWidth() && $this->getWatermarkHeigth() && ($this->getWatermarkPosition() != self::POSITION_STRETCH)) {
                $newWatermark = imagecreatetruecolor($this->getWatermarkWidth(), $this->getWatermarkHeigth());
                imagealphablending($newWatermark, false);
                $col = imagecolorallocate($newWatermark, 255, 255, 255);
                imagefilledrectangle($newWatermark, 0, 0, $this->getWatermarkWidth(), $this->getWatermarkHeigth(), $col);
                imagealphablending($newWatermark, true);

                imagecopyresampled($newWatermark, $watermark, 0, 0, 0, 0, $this->getWatermarkWidth(), $this->getWatermarkHeigth(), imagesx($watermark), imagesy($watermark));
                $watermark = $newWatermark;
            }

            if ($this->getWatermarkPosition() == self::POSITION_TILE) {
                $repeat = true;
            } else if ($this->getWatermarkPosition() == self::POSITION_STRETCH) {
                $newWatermark = imagecreatetruecolor($this->_imageSrcWidth, $this->_imageSrcHeight);
                imagealphablending($newWatermark, false);
                $col = imagecolorallocate($newWatermark, 255, 255, 255);
                imagefilledrectangle($newWatermark, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight, $col);
                imagealphablending($newWatermark, true);

                imagecopyresampled($newWatermark, $watermark, 0, 0, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight, imagesx($watermark), imagesy($watermark));
                $watermark = $newWatermark;
            } else if ($this->getWatermarkPosition() == self::POSITION_TOP_RIGHT) {
                $positionX = ($this->_imageSrcWidth - imagesx($watermark));
                imagecopymerge($this->_imageHandler, $watermark, $positionX, $positionY, 0, 0, imagesx($watermark), imagesy($watermark), $watermarkImageOpacity);
            } else if ($this->getWatermarkPosition() == self::POSITION_BOTTOM_RIGHT) {
                $positionX = ($this->_imageSrcWidth - imagesx($watermark));
                $positionY = ($this->_imageSrcHeight - imagesy($watermark));
                imagecopymerge($this->_imageHandler, $watermark, $positionX, $positionY, 0, 0, imagesx($watermark), imagesy($watermark), $watermarkImageOpacity);
            } else if ($this->getWatermarkPosition() == self::POSITION_BOTTOM_LEFT) {
                $positionY = ($this->_imageSrcHeight - imagesy($watermark));
                imagecopymerge($this->_imageHandler, $watermark, $positionX, $positionY, 0, 0, imagesx($watermark), imagesy($watermark), $watermarkImageOpacity);
            }

            if ($repeat === false) {
                imagecopymerge($this->_imageHandler, $watermark, $positionX, $positionY, 0, 0, imagesx($watermark), imagesy($watermark), $watermarkImageOpacity);
            } else {
                $offsetX = $positionX;
                $offsetY = $positionY;

                while ($offsetY <= ($this->_imageSrcHeight+imagesy($watermark))) {
                    while ($offsetX <= ($this->_imageSrcWidth+imagesx($watermark))) {
                        imagecopymerge($this->_imageHandler, $watermark, $offsetX, $offsetY, 0, 0, imagesx($watermark), imagesy($watermark), $watermarkImageOpacity);
                        $offsetX += imagesx($watermark);
                    }

                    $offsetX = $positionX;
                    $offsetY += imagesy($watermark);
                }
            }

            imagedestroy($watermark);
            $this->refreshImageDimensions();
        }

        /**
         * crop
         *
         * @param       integer     $top
         * @param       integer     $bottom
         * @param       integer     $right
         * @param       integer     $left
         * @return      void
         */
        public function crop($top = 0, $bottom = 0, $right = 0, $left = 0)
        {
            if ($left == 0 && $top == 0 && $right == 0 && $bottom == 0) {
                return;
            }

            $newWidth = $this->_imageSrcWidth - $left - $right;
            $newHeight = $this->_imageSrcHeight - $top - $bottom;

            $canvas = imagecreatetruecolor($newWidth, $newHeight);

            if ($this->_fileType == IMAGETYPE_PNG) {
                $this->_saveAlpha($canvas);
            }

            imagecopyresampled($canvas, $this->_imageHandler, $top, $bottom, $right, $left, $this->_imageSrcWidth, $this->_imageSrcHeight, $newWidth, $newHeight);

            $this->_imageHandler = $canvas;
            $this->refreshImageDimensions();
        }

        /**
         * checkDependencies
         *
         * @throws      Core_Exception
         * @return      void
         */
        public function checkDependencies()
        {
            foreach ($this->_requiredExtensions as $value) {
                if (!extension_loaded($value)) {
                    throw new Core_Exception("Required PHP extension '". $value ."' was not loaded");
                }
            }
        }

    ////////////////////////////////////////////////////////////////////////////
    //                            Private Methods                             //
    ////////////////////////////////////////////////////////////////////////////

        /**
         * Obtain function name, basing on image type and callback type
         *
         * @param       string      $callbackType
         * @param       integer     $fileType
         * @throws      Core_Exception
         * @return      string
         */
        private function _getCallback($callbackType, $fileType = null, $unsupportedText = 'Unsupported image format')
        {
            if (null === $fileType) {
                $fileType = $this->_fileType;
            }

            if (empty(self::$_callbacks[$fileType])) {
                throw new Core_Exception($unsupportedText);
            }

            if (!isset(self::$_callbacks[$fileType][$callbackType])) {
                throw new Core_Exception('Callback not found');
            }

            return self::$_callbacks[$fileType][$callbackType];
        }

        /**
         * _fillBackgroundColor
         *
         * @param       resource        $imageResourceTo
         * @throws      Core_Exception
         * @return      integer
         */
        private function _fillBackgroundColor(&$imageResourceTo)
        {
            // try to keep transparency, if any
            if ($this->_keepTransparency) {
                $isAlpha = false;
                $transparentIndex = $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha);

                try {
                    // fill truecolor png with alpha transparency
                    if ($isAlpha) {
                        if (!imagealphablending($imageResourceTo, false)) {
                            throw new Core_Exception('Failed to set alpha blending for PNG image');
                        }

                        $transparentAlphaColor = imagecolorallocatealpha($imageResourceTo, 0, 0, 0, 127);
                        if (false === $transparentAlphaColor) {
                            throw new Core_Exception('Failed to allocate alpha transparency for PNG image');
                        }

                        if (!imagefill($imageResourceTo, 0, 0, $transparentAlphaColor)) {
                            throw new Core_Exception('Failed to fill PNG image with alpha transparency');
                        }

                        if (!imagesavealpha($imageResourceTo, true)) {
                            throw new Core_Exception('Failed to save alpha transparency into PNG image');
                        }

                        return $transparentAlphaColor;
                    } else if (false !== $transparentIndex) {
                        // fill image with indexed non-alpha transparency
                        list($r, $g, $b)  = array_values(imagecolorsforindex($this->_imageHandler, $transparentIndex));
                        $transparentColor = imagecolorallocate($imageResourceTo, $r, $g, $b);

                        if (false === $transparentColor) {
                            throw new Core_Exception('Failed to allocate transparent color for image');
                        }

                        if (!imagefill($imageResourceTo, 0, 0, $transparentColor)) {
                            throw new Core_Exception('Failed to fill image with transparency');
                        }

                        imagecolortransparent($imageResourceTo, $transparentColor);

                        return $transparentColor;
                    }
                } catch (Exception $e) {
                    // fallback to default background color
                }
            }

            list($r, $g, $b) = $this->_backgroundColor;
            $color = imagecolorallocate($imageResourceTo, $r, $g, $b);

            if (!imagefill($imageResourceTo, 0, 0, $color)) {
                throw new Core_Exception("Failed to fill image background with color ". $r ." ". $g ." ". $b);
            }

            return $color;
        }

        /**
         * _getTransparency
         *
         * @param       resource        $imageResource
         * @param       string          $fileType
         * @param       boolean         $isAlpha
         * @param       boolean         $isTrueColor
         * @return      boolean
         */
        private function _getTransparency($imageResource, $fileType, &$isAlpha = false, &$isTrueColor = false)
        {
            $isAlpha     = false;
            $isTrueColor = false;

            // assume that transparency is supported by gif/png only
            if ((IMAGETYPE_GIF === $fileType) || (IMAGETYPE_PNG === $fileType)) {
                // check for specific transparent color
                $transparentIndex = imagecolortransparent($imageResource);

                if ($transparentIndex >= 0) {
                    return $transparentIndex;
                } else if (IMAGETYPE_PNG === $fileType) {
                    // assume that truecolor PNG has transparency
                    $isAlpha     = true;
                    $isTrueColor = true;

                    return $transparentIndex; // -1
                }
            }

            if (IMAGETYPE_JPEG === $fileType) {
                $isTrueColor = true;
            }

            return false;
        }

        /**
         * refreshImageDimensions
         *
         * @return      void
         */
        private function refreshImageDimensions()
        {
            $this->_imageSrcWidth = imagesx($this->_imageHandler);
            $this->_imageSrcHeight = imagesy($this->_imageHandler);
        }

        /*
         * Fixes saving PNG alpha channel
         *
         * @param       resource        $imageHandler
         * @return      void
         */
        private function _saveAlpha($imageHandler)
        {
            $background = imagecolorallocate($imageHandler, 0, 0, 0);
            ImageColorTransparent($imageHandler, $background);
            imagealphablending($imageHandler, false);
            imagesavealpha($imageHandler, true);
        }
    }